<template>
  <view :class="formItemClass" :style="formItemStyle">
    <view
      v-if="$slots.label || !isNullish(label)"
      :class="bem.e('label')"
      :style="labelStyle"
    >
      <view v-if="shouldShowStar" :class="bem.e('star')">*</view>
      <slot name="label">{{ label }}</slot>
    </view>
    <view :class="bem.e('content')">
      <slot></slot>
      <slot name="validate" :state="validateState"></slot>
      <slot
        name="error"
        :message="validateMessage"
        :show-error="shouldShowError"
      >
        <view v-if="shouldShowError" :class="bem.e('error')">
          {{ validateMessage }}
        </view>
      </slot>
    </view>
  </view>
</template>

<script>
import { mergeDefaults as _mergeDefaults, defineComponent as _defineComponent } from "vue";
import {
  computed,
  ref,
  provide,
  onMounted,
  onBeforeUnmount,
  reactive,
  watch,
  toRef,
  nextTick,
  getCurrentInstance
} from "vue";
import {
  classNames,
  stringifyStyle,
  createBem,
  isNullish,
  chainGet,
  toArray,
  chainSet,
  deepClone,
  noop,
  uniqid,
  getViewportScrollInfo,
  getBoundingClientRect,
  getScrollIntoViewValue,
  getWindowInfo
} from "../../utils";
import {
  formItemContextSymbol,
  useFormContext,
  defaultFormItemProps
} from "../form/common";
/**
 * @property {string} rootClass 组件根元素类名，默认值：-。
 * @property {StyleValue} rootStyle 组件根元素样式，默认值：-。
 * @property {'horizontal' | 'vertical'} direction 表单排列方向，默认值：'horizontal'。
 * @property {string} labelWidth 标签宽度，默认值：-。
 * @property {'start' | 'center' | 'end'} labelAlign 标签水平对齐方式，默认值：'start'。
 * @property {'start' | 'center' | 'end'} labelValign 标签垂直对齐方式，默认值：'start'。
 * @property {'left' | 'right'} starPosition 必填星号在标签的左边或右边，默认值：'left'。
 * @property {string} label 标签文本，默认值：-。
 * @property {boolean} required 是否为必填项，如不设置，则会根据校验规则确认，默认值：-。
 * @property {FieldName} name 表单域 `model` 字段，在使用 `validate、reset` 方法的情况下，该属性是必填的。，默认值：-。
 * @property {Rule | Rule[]} rules 表单验证规则，默认值：-。
 * @property {TriggerType} validateTrigger 设置字段校验的时机，默认值：change。
 * @property {string} error 表单域验证错误时的提示信息。设置该值会导致表单验证状态变为 `error`，并显示该错误信息。，默认值：-。
 * @property {boolean} showError 是否显示校验错误信息，默认值：true。
 * @property {boolean} inlaid 去掉边框和内边距，用于嵌入到其他组件中，默认值：false。
 */
export default  _defineComponent({
  ...{
    options: {
      virtualHost: true,
      styleIsolation: "shared"
    }
  },
  __name: "form-item",
  props:  _mergeDefaults({
    rootStyle: { type: [Boolean, null, String, Object, Array], required: false, skipCheck: true },
    rootClass: { type: String, required: false },
    direction: { type: String, required: false },
    labelWidth: { type: String, required: false },
    labelAlign: { type: String, required: false },
    labelValign: { type: String, required: false },
    starPosition: { type: String, required: false },
    label: { type: String, required: false },
    hideStar: { type: Boolean, required: false },
    required: { type: Boolean, required: false, skipCheck: true },
    name: { type: [String, Number, Array], required: false },
    rules: { type: [Object, Array], required: false },
    validateTrigger: { type: [String, Array], required: false },
    error: { type: String, required: false },
    showError: { type: Boolean, required: false },
    inlaid: { type: Boolean, required: false }
  }, defaultFormItemProps()),
  setup(__props, { expose: __expose }) {
    const props = __props;
    const bem = createBem("form-item");
    const formContext = useFormContext();
    if (!formContext) {
      throw new Error("FormItem must be included in Form.");
    }
    let isResetting = false;
    const fieldValue = computed({
      get() {
        const model = formContext.model;
        if (!model || !props.name) {
          return;
        }
        return chainGet(model, toArray(props.name));
      },
      set(value) {
        const model = formContext.model;
        if (!model || !props.name) {
          return;
        }
        chainSet(model, toArray(props.name), value);
      }
    });
    let initialValue;
    const validateMessage = ref("");
    watch(
      () => props.error,
      () => {
        validateMessage.value = props.error || "";
        validateState.value = props.error ? "error" : "";
      }
    );
    watch(
      () => props.rules,
      () => {
        if (validateMessage.value) {
          validate().catch(noop);
        }
      },
      {
        deep: true,
        flush: "post"
      }
    );
    const shouldShowError = computed(() => {
      return !!props.showError && !!formContext.showError && !!validateMessage.value;
    });
    const validateState = ref("");
    const mergedValidateTrigger = computed(() => {
      const trigger = props.validateTrigger ?? formContext.validateTrigger;
      return trigger ? toArray(trigger) : void 0;
    });
    const mergedRules = computed(() => {
      const rules = [];
      if (props.rules) {
        rules.push(...toArray(props.rules));
      }
      const formRules = formContext.rules;
      if (formRules && props.name) {
        const fRules = chainGet(formRules, toArray(props.name));
        if (fRules) {
          rules.push(...toArray(fRules));
        }
      }
      const required = props.required;
      if (required !== void 0) {
        const requiredRules = rules.map((rule, i) => [rule, i]).filter(([rule]) => {
          return Object.keys(rule).includes("required");
        });
        if (requiredRules.length > 0) {
          for (const [rule, i] of requiredRules) {
            if (rule.required !== required) {
              rules[i] = { ...rule, required };
            }
          }
        } else {
          rules.push({ required });
        }
      }
      const trigger = mergedValidateTrigger.value;
      if (trigger && trigger.length > 0) {
        for (let i = 0, l = rules.length; i < l; i++) {
          const rule = rules[i];
          if (!rule.trigger) {
            rules[i] = { ...rule, trigger: [...trigger] };
          }
        }
      }
      return rules;
    });
    const isRequired = computed(() => {
      return mergedRules.value.some((rule) => rule.required);
    });
    const shouldShowStar = computed(() => {
      return !formContext.hideStar && !props.hideStar && isRequired.value;
    });
    const validate = async (trigger) => {
      if (isResetting || !props.name) {
        return;
      }
      const validRules = formContext.validator.getValidTriggerRules(
        mergedRules.value,
        trigger
      );
      if (validRules.length === 0) {
        return;
      }
      validateState.value = "validating";
      try {
        await formContext.validator.validate(mergedRules.value, {
          validateFirst: true,
          value: fieldValue.value,
          name: props.name,
          label: props.label,
          trigger
        });
        validateState.value = "success";
        validateMessage.value = "";
      } catch (messages) {
        validateState.value = "error";
        validateMessage.value = messages[0];
        const error = {
          name: props.name,
          value: fieldValue.value,
          message: validateMessage.value
        };
        throw error;
      }
    };
    const clearValidate = () => {
      validateState.value = "";
      validateMessage.value = "";
      isResetting = false;
    };
    const reset = async () => {
      isResetting = true;
      fieldValue.value = deepClone(initialValue);
      await nextTick();
      clearValidate();
    };
    const fieldId = uniqid();
    const instance = getCurrentInstance();
    const scrollToField = async () => {
      const [scrollInfo, fieldRect, windowInfo] = await Promise.all([
        getViewportScrollInfo(),
        getBoundingClientRect(`.${fieldId}`, instance),
        getWindowInfo()
      ]);
      const scrollTop = getScrollIntoViewValue(
        windowInfo.windowHeight,
        scrollInfo.scrollTop,
        fieldRect.height,
        fieldRect.top + scrollInfo.scrollTop,
        formContext.scrollIntoViewOptions
      );
      uni.pageScrollTo({
        scrollTop,
        duration: formContext.scrollIntoViewOptions?.duration ?? formContext.scrollDuration
      });
    };
    const onBlur = () => {
      validate("blur").catch(noop);
    };
    const onChange = () => {
      validate("change").catch(noop);
    };
    const context = reactive({
      name: toRef(() => props.name),
      validateMessage,
      validateState,
      validate,
      clearValidate,
      reset,
      scrollToField,
      onBlur,
      onChange
    });
    onMounted(() => {
      if (props.name) {
        initialValue = deepClone(fieldValue.value);
        formContext.addField(context);
      }
    });
    onBeforeUnmount(() => {
      formContext.removeField(context);
    });
    provide(formItemContextSymbol, context);
    __expose({
      validate,
      reset,
      clearValidate,
      scrollToField,
      validateMessage,
      validateState
    });
    const mergedDirection = computed(() => props.direction || formContext.direction);
    const formItemClass = computed(() => {
      return classNames(
        bem.b(),
        bem.m(mergedDirection.value),
        bem.m("inlaid", props.inlaid),
        bem.m(`align-${props.labelAlign || formContext.labelAlign}`),
        bem.m(`valign-${props.labelValign || formContext.labelValign}`),
        bem.m(`star-${props.starPosition || formContext.starPosition}`),
        props.rootClass,
        fieldId
      );
    });
    const formItemStyle = computed(() => {
      return stringifyStyle(props.rootStyle);
    });
    const labelStyle = computed(() => {
      return stringifyStyle({
        width: mergedDirection.value === "horizontal" && (props.labelWidth || formContext.labelWidth)
      });
    });
    const __returned__ = { props, bem, formContext, get isResetting() {
      return isResetting;
    }, set isResetting(v) {
      isResetting = v;
    }, fieldValue, get initialValue() {
      return initialValue;
    }, set initialValue(v) {
      initialValue = v;
    }, validateMessage, shouldShowError, validateState, mergedValidateTrigger, mergedRules, isRequired, shouldShowStar, validate, clearValidate, reset, fieldId, instance, scrollToField, onBlur, onChange, context, mergedDirection, formItemClass, formItemStyle, labelStyle, get isNullish() {
      return isNullish;
    } };
    return __returned__;
  }
});
</script>

<style lang="scss">
@import './index.scss';
</style>